﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SSharp.Core.Evaluator;
using SSharp.Core.DataTypes;

namespace SSharp.Core.Builtins.Macros {
	public class Let : Macro {
		private static readonly Symbol defineSymbol = new Symbol("define");
		private static readonly Symbol beginSymbol = new Symbol("begin");
		private static readonly Symbol lambdaSymbol = new Symbol("lambda");

		public object Expand(IList<object> args) {
			return Expand((IEnumerable<object>)args);
		}

		/// <summary>
		/// Expands the let macro
		/// </summary>
		private object Expand(IEnumerable<object> args) {
			Symbol procid = args.First() as Symbol;
			if (procid != null) {
				args = args.Skip(1);
			}

			object varsObject = args.First();
			List<KeyValuePair<Symbol, object>> vars = parseVars(varsObject);
			IEnumerable<Symbol> varKeys = vars.Select(v => v.Key);
			IEnumerable<object> varValues = vars.Select(v => v.Value);
			args = args.Skip(1);

			object body = new Cons(beginSymbol, List.Create(args));

			object lambda = List.Create(
				lambdaSymbol,
				List.Create(varKeys),
				body);

			if (procid == null) {
				// simple case - just call lambda with definitions
				return new Cons(
					lambda,
					List.Create(varValues));
			} else {
				// if let has a procid we must define a function with that id and only then call it
				// we convert this to:
				// ((lambda ()		; first lambda is just to create a frame for the proc to be defined in
				//    (define procid (lambda (varKeys) body))
				//    (procid varValues)))
				return List.Create(
					List.Create(
						lambdaSymbol,
						List.EmptyList.Instance,
						List.Create(
							defineSymbol,
							procid,
							lambda
						),
						new Cons(
							procid,
							List.Create(varValues)
						)
					)
				);
			}
		}

		/// <summary>
		/// Parses the variable part of the let e.g. ([x 1] [y 2]) to a list of keys and values.
		/// Returns a list and not a dictionary to preserve order; makes sure variables are unique.
		/// </summary>
		private List<KeyValuePair<Symbol, object>> parseVars(object varsObject) {
			IList<object> varsList = List.Unpack(varsObject);
			if (varsList == null) {
				throw new SyntaxError("let: invalid variable list");
			}

			List<KeyValuePair<Symbol, object>> result = new List<KeyValuePair<Symbol, object>>();
			HashSet<Symbol> seenSymbols = new HashSet<Symbol>();
			foreach (object varObject in varsList) {
				IList<object> varList = List.Unpack(varObject);
				if (varList == null) {
					throw new SyntaxError("let: invalid variable definition " + varObject);
				}

				if (varList.Count != 2) {
					throw new SyntaxError("let: each variable definition must be in the form (var value); given " + varObject);
				}

				Symbol var = varList[0] as Symbol;
				object value = varList[1];

				if (var == null) {
					throw new SyntaxError("let: invalid variable " + varList[0]);
				}

				if (seenSymbols.Contains(var)) {
					throw new SyntaxError("let: duplicate definition for " + var);
				}

				seenSymbols.Add(var);
				result.Add(new KeyValuePair<Symbol, object>(var, value));
			}

			return result;
		}
	}
}
